package net.jalbum.editor;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.dnd.DropTargetEvent;
import java.awt.dnd.DropTargetListener;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.event.WindowFocusListener;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.prefs.Preferences;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.DefaultListCellRenderer;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JTabbedPane;
import javax.swing.KeyStroke;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import org.fife.ui.autocomplete.CompletionProvider;
import org.fife.ui.autocomplete.DefaultCompletionProvider;
import org.fife.ui.autocomplete.MarkupTagCompletion;
import org.fife.ui.autocomplete.VariableCompletion;
import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
import org.fife.ui.rtextarea.SearchEngine;
import se.datadosen.component.JStackLabel;

public class JTextPad extends JFrame {

    public final static String APP_NAME = "jAlbum Textpad";
    public final static String VERSION = "2.2";
    static final int menuShortcutKeyMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(); // Convenience
    private static JTextPad instance;
    static LinkedList<JTextPad> editors = new LinkedList<JTextPad>();
    private List<JTextPadDocument> documents = new ArrayList<JTextPadDocument>();
//    private JTextPadDocument currentDocument;
    private JTabbedPane tabs = new JTabbedPane();
    private DefaultCompletionProvider provider = new DefaultCompletionProvider() {

        @Override
        protected boolean isValidChar(char ch) {
            if (ch == ':') {
                return true; // Support html ja: elements too
            }
            return super.isValidChar(ch);
        }
    };

    CompletionProvider getCompletionProvider() {
        return provider;
    }

    public static JTextPad getInstance() {
        if (instance == null) {
            instance = new JTextPad();
        }
        return instance;
    }

    public static JTextPad getLastUsedInstance() {
        if (editors.size() == 0) {
            return getInstance();
        }
        return editors.getFirst();
    }
    public static Map<String, String> fileTypes = new HashMap<String, String>();

    static {
        fileTypes.put("txt", SyntaxConstants.SYNTAX_STYLE_NONE);
        fileTypes.put("jsp", SyntaxConstants.SYNTAX_STYLE_JSP);
        fileTypes.put("htt", SyntaxConstants.SYNTAX_STYLE_JSP);
        fileTypes.put("inc", SyntaxConstants.SYNTAX_STYLE_JSP);
        fileTypes.put("java", SyntaxConstants.SYNTAX_STYLE_JAVA);
        fileTypes.put("bsh", SyntaxConstants.SYNTAX_STYLE_JAVA);
        fileTypes.put("css", SyntaxConstants.SYNTAX_STYLE_CSS);
        fileTypes.put("xml", SyntaxConstants.SYNTAX_STYLE_XML);
        fileTypes.put("info", SyntaxConstants.SYNTAX_STYLE_XML);
        fileTypes.put("rss", SyntaxConstants.SYNTAX_STYLE_XML);
        fileTypes.put("html", SyntaxConstants.SYNTAX_STYLE_HTML);
        fileTypes.put("htm", SyntaxConstants.SYNTAX_STYLE_HTML);
        fileTypes.put("xhtml", SyntaxConstants.SYNTAX_STYLE_HTML);
        fileTypes.put("properties", SyntaxConstants.SYNTAX_STYLE_PROPERTIES_FILE);
        fileTypes.put("jap", SyntaxConstants.SYNTAX_STYLE_PROPERTIES_FILE);
        fileTypes.put("js", SyntaxConstants.SYNTAX_STYLE_JAVASCRIPT);
    }
    static KeyStroke ctrlPgUp = KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, menuShortcutKeyMask);
    static KeyStroke ctrlPgDown = KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, menuShortcutKeyMask);
    Action previousDocumentAction = new AbstractAction("Previous document") {

        {
            putValue(Action.ACCELERATOR_KEY, ctrlPgUp);
        }

        public void actionPerformed(ActionEvent e) {
            int index = tabs.getSelectedIndex();
            if (index > 0) {
                tabs.setSelectedIndex(index - 1);
            }
        }
    };
    Action nextDocumentAction = new AbstractAction("Next document") {

        {
            putValue(Action.ACCELERATOR_KEY, ctrlPgDown);
        }

        public void actionPerformed(ActionEvent e) {
            int index = tabs.getSelectedIndex();
            if (index < tabs.getTabCount() - 1) {
                tabs.setSelectedIndex(index + 1);
            }
        }
    };
    Action saveAllAction = new AbstractAction("Save All") {

        {
            putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_S, menuShortcutKeyMask | KeyEvent.ALT_DOWN_MASK));
        }

        public void actionPerformed(ActionEvent e) {
            for (JTextPadDocument document : documents) {
                document.saveAction.actionPerformed(null);
            }
        }
    };
    /**
     * Edit actions
     */
    Action findAction = new AbstractAction("Find...") {

        {
            putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_F, menuShortcutKeyMask));
        }

        public void actionPerformed(ActionEvent e) {
            findDialog.setMode(JFindDialog.Mode.find);
            findDialog.setVisible(true);

        }
    };
    Action replaceAction = new AbstractAction("Replace...") {

        {
            putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_R, menuShortcutKeyMask));
        }

        public void actionPerformed(ActionEvent e) {
            findDialog.setMode(JFindDialog.Mode.replace);
            findDialog.setVisible(true);
        }
    };
    Action findNextAction = new AbstractAction("Find Next") {

        {
            putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_F3, 0));
        }

        public void actionPerformed(ActionEvent e) {
            if (findDialog.toFind.getText().length() == 0) {
                findAction.actionPerformed(e);
            } else {
                boolean found = false;
                boolean up = findDialog.up.isSelected();
                JTextPadDocument document = getCurrentDocument();
                do {
                    int oldCaretPosition = document.getTextArea().getCaretPosition();
                    if (document != getCurrentDocument()) {
                        document.getTextArea().setCaretPosition(up ? document.getTextArea().getDocument().getLength() : 0);
                    }

                    found = SearchEngine.find(document.getTextArea(),
                            findDialog.toFind.getText(),
                            findDialog.down.isSelected(),
                            findDialog.matchCase.isSelected(),
                            findDialog.wholeWords.isSelected(),
                            findDialog.regExp.isSelected());
                    if (found) {
                        if (document != getCurrentDocument()) {
                            setCurrentDocument(document);
                        }
                        return;
                    } else {
                        document.getTextArea().setCaretPosition(oldCaretPosition);
                    }

                } while (findDialog.allDocuments.isSelected() && (document = getAdjacentDocument(document, up)) != null);
                if (!found) {
                    getStatusBar().setText("Search passed the end of file");
                }
            }
        }
    };
    Action replaceNextAction = new AbstractAction("Replace Next") {

        public void actionPerformed(ActionEvent e) {
            if (findDialog.toFind.getText().length() == 0) {
                findAction.actionPerformed(e);
            } else {
                boolean found = false;
                boolean up = findDialog.up.isSelected();
                JTextPadDocument document = getCurrentDocument();
                do {
                    int oldCaretPosition = document.getTextArea().getCaretPosition();
                    if (document != getCurrentDocument()) {
                        document.getTextArea().setCaretPosition(up ? document.getTextArea().getDocument().getLength() : 0);
                    }

                    found = SearchEngine.replace(document.getTextArea(),
                            findDialog.toFind.getText(),
                            findDialog.replaceWith.getText(),
                            findDialog.down.isSelected(),
                            findDialog.matchCase.isSelected(),
                            findDialog.wholeWords.isSelected(),
                            findDialog.regExp.isSelected());
                    if (found) {
                        if (document != getCurrentDocument()) {
                            setCurrentDocument(document);
                        }
                        return;
                    } else {
                        document.getTextArea().setCaretPosition(oldCaretPosition);
                    }

                } while (findDialog.allDocuments.isSelected() && (document = getAdjacentDocument(document, up)) != null);
                if (!found) {
                    getStatusBar().setText("Search passed the end of file");
                }
            }
        }
    };
    Action replaceAllAction = new AbstractAction("Replace All") {

        public void actionPerformed(ActionEvent e) {
            if (findDialog.toFind.getText().length() == 0) {
                return;
            } else {
                List<JTextPadDocument> docs = new LinkedList<JTextPadDocument>();
                if (findDialog.allDocuments.isSelected()) {
                    docs.addAll(documents);
                } else {
                    docs.add(getCurrentDocument());
                }

                int total = 0;
                int nDocs = 0;
                for (JTextPadDocument document : docs) {
                    int count = SearchEngine.replaceAll(document.getTextArea(),
                            findDialog.toFind.getText(),
                            findDialog.replaceWith.getText(),
                            findDialog.matchCase.isSelected(),
                            findDialog.wholeWords.isSelected(),
                            findDialog.regExp.isSelected());
                    total += count;
                    if (count > 0) {
                        nDocs++;
                    }
                }
                getStatusBar().setText("" + total + " occurences of " + findDialog.toFind.getText() + " in " + nDocs + " documents replaced");
            }
        }
    };
    Action exitAction = new AbstractAction("Exit") {

        {
            putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_Q, menuShortcutKeyMask));
        }

        public void actionPerformed(ActionEvent e) {
            try {
                saveWindowLocation();

                setVisible(false);
                List<JTextPadDocument> docsClone = new ArrayList<JTextPadDocument>(documents);
                for (JTextPadDocument document : docsClone) {
                    closeDocument(document, true);
                }
                editors.remove(JTextPad.this);

                if (Main.runsStandalone && editors.size() == 0) {
                    System.exit(0);
                } else {
                    setVisible(false);
                }
            } catch (OperationCanceledException ex) {}
        }
    };
    private JFindDialog findDialog = new JFindDialog(this);

    public JTextPad() {
        init();
    }

    public JTextPad(File workingFile) {
        init();
        openDocument(workingFile);
    }

    public File getCurrentFile() {
        JTextPadDocument doc = getCurrentDocument();
        return doc != null ? doc.getFile() : null;
    }

    public JTextPadDocument getCurrentDocument() {
        return (JTextPadDocument) tabs.getSelectedComponent();
    }

    List<JTextPadDocument> getDocuments() {
        return documents;
    }

    public void setCurrentDocument(JTextPadDocument document) {
        tabs.setSelectedComponent(document);
        document.getTextArea().requestFocusInWindow();
    }

    public JTextPadDocument getOpenDocumentFor(File f) {
        for (JTextPadDocument d : documents) {
            if (f.equals(d.getFile())) {
                return d;
            }
        }
        return null;
    }

    public JTextPadDocument openDocument(File f) {
        try {
            if (f != null) {
                JTextPadDocument document = getOpenDocumentFor(f);
                if (document != null) {
                    return document;
                }
            }
            // Not found, so add new
            JTextPadDocument document = new JTextPadDocument(this, f);
            tabs.addTab(null, document);
            tabs.setTabComponentAt(tabs.getTabCount() - 1, document.getTabComponent());
            if (document.getFile() != null) {
                tabs.setToolTipTextAt(tabs.getTabCount() - 1, document.getFile().getAbsolutePath());
            }
            documents.add(document);
            if (documents.size() == 1) {
                setVisible(true);
                document.getTextArea().requestFocusInWindow();
            } else {
                toFront();
            }
            return document;
        } catch (NoSuchMethodError ex) {
            JOptionPane.showMessageDialog(this, "Sorry, the editor requires at least Java 1.6", "Missing functionality", JOptionPane.ERROR_MESSAGE);
            throw ex;
        }
    }

    public void closeDocument(JTextPadDocument document) throws OperationCanceledException {
        closeDocument(document, false);
    }

    private void saveWindowLocation() {
        Preferences prefs = Preferences.userNodeForPackage(JTextPad.class);
        Rectangle bounds = getBounds();
        prefs.putInt("windowX", bounds.x);
        prefs.putInt("windowY", bounds.y);
        prefs.putInt("windowWidth", bounds.width);
        prefs.putInt("windowHeight", bounds.height);
    }

    private void loadWindowLocation() {
        Preferences prefs = Preferences.userNodeForPackage(JTextPad.class);
        Dimension size = new Dimension(prefs.getInt("windowWidth", 800),
                prefs.getInt("windowHeight", 600));
        setSize(size);
        Point location = new Point(prefs.getInt("windowX", -1),
                prefs.getInt("windowY", -1));
        if (location.x > 0) {
            setLocation(location);
        } else {
            setLocationRelativeTo(null);
        }
    }

    private JTextPadDocument getAdjacentDocument(JTextPadDocument document, boolean previous) {
        int index = tabs.indexOfComponent(document);
        if (previous) {
            if (index > 0) {
                return (JTextPadDocument) tabs.getComponentAt(index - 1);
            }
        } else {
            if (index < tabs.getTabCount() - 1) {
                return (JTextPadDocument) tabs.getComponentAt(index + 1);
            }
        }
        return null;
    }

    void closeDocument(JTextPadDocument document, boolean inExit) throws OperationCanceledException {
        document.ensureNoLoss();
        tabs.removeTabAt(tabs.indexOfComponent(document));
        documents.remove(document);
        if (documents.size() == 0 && !inExit) {
            exitAction.actionPerformed(null);
        }
    }

    private JStackLabel getStatusBar() {
        return getCurrentDocument().getStatusBar();
    }

    private void init() {
        setApplicationModalExclusion(this);

        // Implement DnD file support

        try {
            setDropTarget(new DropTarget(JTextPad.this, new DropTargetListener() {

                public void dragEnter(DropTargetDragEvent e) {
                    e.acceptDrag(DnDConstants.ACTION_COPY);
                }

                public void dragOver(DropTargetDragEvent e) {
                    e.acceptDrag(DnDConstants.ACTION_COPY);
                }

                public void dragExit(DropTargetEvent e) {
                }

                public void drop(DropTargetDropEvent e) {
                    if ((e.getSourceActions() & DnDConstants.ACTION_COPY) != 0) {
                        e.acceptDrop(DnDConstants.ACTION_COPY);
                    } else {
                        e.rejectDrop();
                        return;

                    }
                    try {
                        List<File> files = (List<File>) e.getTransferable().getTransferData(DataFlavor.javaFileListFlavor);
                        e.dropComplete(true);
                        boolean first = true;
                        for (File f : files) {
                            JTextPadDocument document = openDocument(f);
                            if (first) {
                                setCurrentDocument(document);
                                first = false;
                            }
                        }
                        return;

                    } catch (java.io.IOException ex) {
                        System.err.println(ex);
                    } catch (UnsupportedFlavorException ex) {
                        System.err.println(ex);
                    }

                    e.dropComplete(false);
                }

                public void dropActionChanged(DropTargetDragEvent e) {
                }
            }));
        } catch (RuntimeException ex) {
            ex.printStackTrace(System.err);
        }

        // Main completion provider stuff
        try {
            provider.loadFromXML(JTextPad.class.getResourceAsStream("jalbum.xml"));
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }

        provider.setListCellRenderer(new DefaultListCellRenderer() {

            private final Color darkGreen = new Color(50, 140, 50);

            @Override
            public Component getListCellRendererComponent(
                    JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
                Component comp = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
                if (!isSelected) {
                    if (value instanceof MarkupTagCompletion) {
                        comp.setForeground(Color.blue);
                    } else if ((value instanceof VariableCompletion)) {
                        comp.setForeground(darkGreen);
                    }
                }
                return comp;
            }
        });


        addWindowFocusListener(new WindowFocusListener() {

            public void windowGainedFocus(WindowEvent e) {
                if (isVisible()) {
                    editors.remove(JTextPad.this);
                    editors.addFirst(JTextPad.this);
                }
            }

            public void windowLostFocus(WindowEvent e) {
            }
        });

        setTitle(APP_NAME);
        tabs.addChangeListener(new ChangeListener() {

            public void stateChanged(ChangeEvent e) {
                JTextPadDocument document = getCurrentDocument();
                if (document != null) {
                    setJMenuBar(document.getMenuBar());
                    getRootPane().putClientProperty("Window.documentFile", document.getFile()); // Mac OS X
                    updateTitle();
                }
            }
        });

        Container c = getContentPane();
        c.setLayout(new BorderLayout());
        c.add(tabs, BorderLayout.CENTER);

        pack();
        loadWindowLocation();
        findDialog.setLocationRelativeTo(this);
    }

    private static boolean setApplicationModalExclusion(JFrame frmMain) {
        try {
            Class exclusionType = Class.forName("java.awt.Dialog$ModalExclusionType");
            Field field = exclusionType.getField("APPLICATION_EXCLUDE");
            Object value = field.get(exclusionType);

            Method meth = JFrame.class.getMethod(
                    "setModalExclusionType",
                    new Class[]{exclusionType});
            meth.invoke(frmMain, value);
            return true;
        } catch (Exception e) {
        }
        return false;
    }

    /**
     * Overridden so we can ask before exiting.
     */
    @Override
    protected void processWindowEvent(WindowEvent e) {
        if (e.getID() == WindowEvent.WINDOW_CLOSING) {
            exitAction.actionPerformed(null);
        } else {
            // All other events are handled as usual
            super.processWindowEvent(e);
        }
    }

    void updateTitle() {
        String name = "";
        JTextPadDocument document = getCurrentDocument();
        if (document != null) {
            File wf = document.getFile();
            if (wf != null) {
                File parent = wf.getParentFile();
                if (parent != null) {
                    name = parent.getName() + File.separatorChar;
                }
                name += wf.getName();
            }
        }
        setTitle(JTextPad.APP_NAME + ("".equals(name) ? "" : " [" + name + "]"));
    }

    public void actionPerformed(ActionEvent e) {
        throw new UnsupportedOperationException("Not supported yet.");
    }
}
